/*
 *    GeoTools - The Open Source Java GIS Toolkit
 *    http://geotools.org
 *
 *    (C) 2008-2011, Open Source Geospatial Foundation (OSGeo)
 *
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation;
 *    version 2.1 of the License.
 *
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *    Lesser General Public License for more details.
 */

package org.geotools.geotools_swing.old;

import java.awt.Dimension;
import java.awt.Point;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.concurrent.SynchronousQueue;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.JViewport;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;

import net.miginfocom.swing.MigLayout;

import org.geotools.referencing.CRS;
import org.geotools.util.logging.Logging;

import org.opengis.referencing.crs.CoordinateReferenceSystem;

/**
 * This class has a single static method that shows a dialog to prompt
 * the user to choose a coordinate reference system. 
 * <p>
 * Example of use:
 * <pre><code>
 * CoordinateReferenceSystem crs = JCRSChooser.showDialog();
 * if (crs != null) {
 *     // use the CRS...
 * }
 * </code></pre>
 *
 * @author Michael Bedward
 * @since 2.6
 *
 * @source $URL$
 * @version $Id$
 */
public class JCRSChooser {
    private static final Logger LOGGER = Logging.getLogger("org.geotools.swing");
    
    /** Default authority name (EPSG). */
    public static final String DEFAULT_AUTHORITY = "EPSG";
    
    /** Default dialog title */
    public static final String DEFAULT_TITLE = "Choose a projection";
    
    @SuppressWarnings("unused")
	private CRSDialog dialog;
    @SuppressWarnings("unused")
	private CoordinateReferenceSystem crs;
    
    /**
     * Constructor is hidden.
     */
    private JCRSChooser() {}
    
    /**
     * Displays a dialog with a list of coordinate reference systems in the EPSG
     * database. 
     * <p>
     * This method can be called safely from any thread.
     *
     * @return a {@code CoordinateReferenceSystem} object or {@code null} if the user
     *         cancelled the dialog
     */
    public static CoordinateReferenceSystem showDialog() {
        return showDialog(null);
    }
    
    /**
     * Displays a dialog with a list of coordinate reference systems in the EPSG
     * database. 
     * <p>
     * This method can be called safely from any thread.
     *
     * @param title optional non-default title
     *
     * @return a {@code CoordinateReferenceSystem} object or {@code null} if the user
     *         cancelled the dialog
     */
    public static CoordinateReferenceSystem showDialog(final String title) {
        return showDialog(title, null);
    }
    
    /**
     * Displays a dialog with a list of coordinate reference systems in the EPSG
     * database and with the specified initial code highlighted.
     * <p>
     * This method can be called safely from any thread.
     *
     * @param title optional non-default title
     * @param initialCode optional initial EPSG code
     *
     * @return a {@code CoordinateReferenceSystem} object or {@code null} if the user
     *         cancelled the dialog
     */
    public static CoordinateReferenceSystem showDialog(final String title, 
            final String initialCode) {
        
        return showDialog(title, initialCode, null);
    }

    /**
     * Displays a dialog with a list of coordinate reference systems provided by
     * the given authority (e.g. "EPSG"), and with the specified initial code
     * highlighted.
     * <p>
     * This method can be called safely from any thread.
     *
     * @param title optional non-default title
     * @param initialCode an optional initial code in appropriate form for the authority
     * @param authority optional non-default authority (defaults to "EPSG")
     *
     * @return a {@code CoordinateReferenceSystem} object or {@code null} if the user
     *         cancelled the dialog
     */
    public static CoordinateReferenceSystem showDialog(final String title, 
            final String initialCode, 
            final String authority) {

        CoordinateReferenceSystem selected = null;
        
        if (SwingUtilities.isEventDispatchThread()) {
            selected = doShow(title, initialCode, authority);
            
        } else {
            final SynchronousQueue<CoordinateReferenceSystem> sq =
                    new SynchronousQueue<CoordinateReferenceSystem>();
            
            final Thread currentThread = Thread.currentThread();
            
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    try {
                        CoordinateReferenceSystem crs = 
                                doShow(title, initialCode, authority);
                        if (crs == null) {
                            currentThread.interrupt();
                        } else {
                            sq.put(crs);
                        }
                    } catch (InterruptedException ex) {
                        throw new RuntimeException(ex);
                    }
                }
            });
            
            try {
                selected = sq.take();
            } catch (InterruptedException ex) {
                // dialog was cancelled
            }
        }
        
        return selected;
    }
    
    /**
     * Creates and displays the modal dialog.
     * 
     * @param title optional non-default title
     * @param initialCode an optional initial code in appropriate form for the authority
     * @param authority optional non-default authority (defaults to "EPSG")
     * 
     * @return the selected coordinate reference system or {@code null} if the dialog
     *     is cancelled by the user
     */
    private static CoordinateReferenceSystem doShow(String title, 
            String initialCode, String authority) {
        
        CRSDialog dialog = new CRSDialog(title, initialCode, authority);
        DialogUtils.showCentred(dialog);
        
        CoordinateReferenceSystem crs = dialog.getCoordinateReferenceSystem();
        dialog.dispose();
        
        return crs;
    }

    /**
     * A modal dialog which displays a list of projections for the user to choose from.
     * <p>
     * This class is package-private, rather than private, for unit testing
     * purposes.
     */
    static class CRSDialog extends AbstractSimpleDialog {
        /**
		 * 
		 */
		private static final long serialVersionUID = 1L;

		private static final int CONTROL_WIDTH = 400;

        private final String authority;
        private final String initialCode;
        
        private CRSListModel model;
        private JList<Object> listBox;
        private JButton okButton;
        
        private CoordinateReferenceSystem crs;

        /**
         * Creates the dialog.
         * 
         * @param title optional non-default title
         * @param initialCode an optional initial code in appropriate form for the authority
         * @param authority optional non-default authority (defaults to "EPSG")
         */
        public CRSDialog(String title, String initialCode, String authority) {
            super(DialogUtils.getString(title, DEFAULT_TITLE));
            this.authority = DialogUtils.getString(authority, DEFAULT_AUTHORITY);
            this.initialCode = initialCode;
            
            initComponents();
        }

        @Override
        public JPanel createControlPanel() {
            JPanel panel = new JPanel(new MigLayout("", "[left]"));
            
            model = new CRSListModel(authority);
            
            panel.add(new JLabel("Enter sub-string to filter list"), "growx, wrap");
        
            final JTextField filterFld = new JTextField();
            filterFld.setPreferredSize(new Dimension(CONTROL_WIDTH, 20));
            filterFld.getDocument().addDocumentListener(new DocumentListener() {

                @Override
                public void insertUpdate(DocumentEvent e) {
                    model.setFilter(filterFld.getText());
                }

                @Override
                public void removeUpdate(DocumentEvent e) {
                    model.setFilter(filterFld.getText());
                }

                @Override
                public void changedUpdate(DocumentEvent e) {
                    model.setFilter(filterFld.getText());
                }
            });
            
            panel.add(filterFld, "wrap");

            listBox = new JList<Object>(model);
            listBox.addMouseListener(new MouseAdapter() {

                @Override
                public void mouseClicked(MouseEvent e) {
                    if (e.getClickCount() == 2) {
                        selectCRS(listBox.getSelectedIndex());
                    }
                }
            });
            
            listBox.addListSelectionListener(new ListSelectionListener() {
                @Override
                public void valueChanged(ListSelectionEvent e) {
                    setOKButtonState();
                }
            });
            
            model.addListDataListener(new CRSListModelListener() {
                @Override
                public void process() {
                    setOKButtonState();
                }
            });

            JScrollPane listPane = new JScrollPane(listBox);
            listPane.setPreferredSize(new Dimension(CONTROL_WIDTH, 300));

            listBox.setBorder(BorderFactory.createEtchedBorder());
            listBox.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);

            int initialIndex = model.findCode(initialCode);
            if (initialIndex >= 0) {
                listBox.setSelectedIndex(initialIndex);
                Point p = listBox.indexToLocation(initialIndex);
                JViewport port = listPane.getViewport();
                port.setViewPosition(p);
            }

            panel.add(listPane, "gaptop 10, wrap");
            
            return panel;
        }

        /**
         * Overridden to get a reference to the OK button created by the 
         * super-class method.
         * 
         * @return the button panel
         */
        @Override
        protected JPanel createButtonPanel() {
            JPanel panel = super.createButtonPanel();
            for (JButton btn : DialogUtils.getChildComponents(JButton.class, panel, false)) {
                if ("OK".equals(btn.getText())) {
                    okButton = btn;
                    break;
                }
            }
            
            if (okButton == null) {
                throw new IllegalStateException("Failed to initialize the OK button correctly");
            }
            
            okButton.setEnabled(false);
            
            return panel;
        }

        /**
         * Records the selected coordinate reference system, if one exists,
         * and hides the dialog.
         */
        @Override
        public void onOK() {
            if (model.getSize() > 0 && listBox.getSelectedIndex() >= 0) {
                if (model.getSize() == 1) {
                    selectCRS(0);
                } else {
                    int index = listBox.getSelectedIndex();
                    if (index >= 0) {
                        selectCRS(index);
                    }
                }
                
                setVisible(false);
            }
        }

        /**
         * Sets the selection coordinate reference system to {@code null}
         * and hides the dialog.
         */
        @Override
        public void onCancel() {
            crs = null;
            setVisible(false);
        }

        /**
         * Helper method for the list box and {@linkplain #onOK()} method
         * which records the selected coordinate reference system.
         * 
         * @param index selected item index in the list box
         */
        private void selectCRS(int index) {
            String code = model.getCodeAt(index);
            try {
                crs = CRS.decode(DEFAULT_AUTHORITY + ":" + code, true);

            } catch (Exception ex) {
                LOGGER.log(Level.SEVERE,
                        "Failed to get coordinate reference system for code {0}",
                        code);

            } finally {
                closeDialog();
            }
        }
        
        /**
         * Enables or disables the OK button based on the state
         * of the CRS list.
         */
        private void setOKButtonState() {
            boolean b = model.getSize() == 1 || listBox.getSelectedIndex() >= 0;
            okButton.setEnabled(b);
        }

        /**
         * Gets the selected coordinate reference system.
         * 
         * @return selected coordinate reference system (may be {@code null}).
         */
        CoordinateReferenceSystem getCoordinateReferenceSystem() {
            return crs;
        }
    }
    
    
    /**
     * Simple listener used by the dialog to detect when the list model has
     * changed.
     */
    private static abstract class CRSListModelListener implements ListDataListener {
        
        public abstract void process();

        @Override
        public void intervalAdded(ListDataEvent e) {
            process();
        }

        @Override
        public void intervalRemoved(ListDataEvent e) {
            process();
        }

        @Override
        public void contentsChanged(ListDataEvent e) {
            process();
        }
        
    }

}
